/*
 * @(#)HttpOutputStream.java   0.3-3 06/05/2001
 *
 *  This file is part of the HTTPClient package
 *  Copyright (C) 1996-2001 Ronald Tschal�r
 *
 *  This library is free software; you can redistribute it and/or
 *  modify it under the terms of the GNU Lesser General Public
 *  License as published by the Free Software Foundation; either
 *  version 2 of the License, or (at your option) any later version.
 *
 *  This library is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 *  Lesser General Public License for more details.
 *
 *  You should have received a copy of the GNU Lesser General Public
 *  License along with this library; if not, write to the Free
 *  Software Foundation, Inc., 59 Temple Place, Suite 330, Boston,
 *  MA 02111-1307, USA
 *
 *  For questions, suggestions, bug-reports, enhancement-requests etc.
 *  I may be contacted at:
 *
 *  ronald@innovation.ch
 *
 *  The HTTPClient's home page is located at:
 *
 *  http://www.innovation.ch/java/HTTPClient/
 *
 */

package org.everrest.http.client;

import org.everrest.core.util.Logger;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;

/**
 * This class provides an output stream for requests. The stream must first be
 * associated with a request before it may be used; this is done by passing it
 * to one of the request methods in HTTPConnection. Example:
 *
 * <PRE>
 *    OutputStream out = new HttpOutputStream(12345);
 *    rsp = con.Post(&quot;/cgi-bin/my_cgi&quot;, out);
 *    out.write(...);
 *    out.close();
 *    if (rsp.getStatusCode() &gt;= 300)
 *        ...
 * </PRE>
 * <P>
 * There are two constructors for this class, one taking a length parameter, and
 * one without any parameters. If the stream is created with a length then the
 * request will be sent with the corresponding Content-length header and
 * anything written to the stream will be written on the socket immediately.
 * This is the preferred way. If the stream is created without a length then one
 * of two things will happen: if, at the time of the request, the server is
 * known to understand HTTP/1.1 then each write() will send the data immediately
 * using the chunked encoding. If, however, either the server version is unknown
 * (because this is first request to that server) or the server only understands
 * HTTP/1.0 then all data will be written to a buffer first, and only when the
 * stream is closed will the request be sent.
 * <P>
 * Another reason that using the <var>HttpOutputStream(length)</var> constructor
 * is recommended over the <var>HttpOutputStream()</var> one is that some
 * HTTP/1.1 servers do not allow the chunked transfer encoding to be used when
 * POSTing to a cgi script. This is because the way the cgi API is defined the
 * cgi script expects a Content-length environment variable. If the data is sent
 * using the chunked transfer encoding however, then the server would have to
 * buffer all the data before invoking the cgi so that this variable could be
 * set correctly. Not all servers are willing to do this.
 * <P>
 * If you cannot use the <var>HttpOutputStream(length)</var> constructor and are
 * having problems sending requests (usually a 411 response) then you can try
 * setting the system property <var>HTTPClient.dontChunkRequests</var> to
 * <var>true</var> (this needs to be done either on the command line or
 * somewhere in the code before the HTTPConnection is first accessed). This will
 * prevent the client from using the chunked encoding in this case and will
 * cause the HttpOutputStream to buffer all the data instead, sending it only
 * when close() is invoked.
 * <P>
 * The behaviour of a request sent with an output stream may differ from that of
 * a request sent with a data parameter. The reason for this is that the various
 * modules cannot resend a request which used an output stream. Therefore such
 * things as authorization and retrying of requests won't be done by the
 * HTTPClient for such requests. But see {@link HTTPResponse#retryRequest()
 * HTTPResponse.retryRequest} for a partial solution.
 *
 * @version 0.3-3 06/05/2001
 * @author Ronald Tschal�r
 * @since V0.3
 */
public class HttpOutputStream extends OutputStream
{
   /** null trailers */
   private static final NVPair[] empty = new NVPair[0];

   /** the length of the data to be sent */
   private int length;

   /** the length of the data received so far */
   private int rcvd = 0;

   /** the request this stream is associated with */
   private Request req = null;

   /** the response from sendRequest if we stalled the request */
   private Response resp = null;

   /** the socket output stream */
   private OutputStream os = null;

   /** the buffer to be used if needed */
   private ByteArrayOutputStream bos = null;

   /** the trailers to send if using chunked encoding. */
   private NVPair[] trailers = empty;

   /** the timeout to pass to SendRequest() */
   private int con_to = 0;

   /** just ignore all the data if told to do so */
   private boolean ignore = false;

   private static final Logger log = Logger.getLogger(HttpOutputStream.class);

   // Constructors

   /**
    * Creates an output stream of unspecified length. Note that it is
    * <strong>highly</strong> recommended that this constructor be avoided where
    * possible and <code>HttpOutputStream(int)</code> used instead.
    *
    * @see HttpOutputStream#HttpOutputStream(int)
    */
   public HttpOutputStream()
   {
      length = -1;
   }

   /**
    * This creates an output stream which will take <var>length</var> bytes of
    * data.
    *
    * @param length the number of bytes which will be sent over this stream
    */
   public HttpOutputStream(int length)
   {
      if (length < 0)
         throw new IllegalArgumentException("Length must be greater equal 0");
      this.length = length;
   }

   // Methods

   /**
    * Associates this stream with a request and the actual output stream. No
    * other methods in this class may be invoked until this method has been
    * invoked by the HTTPConnection.
    *
    * @param req the request this stream is to be associated with
    * @param os the underlying output stream to write our data to, or null if we
    *        should write to a ByteArrayOutputStream instead.
    * @param con_to connection timeout to use in sendRequest()
    */
   void goAhead(Request req, OutputStream os, int con_to)
   {
      this.req = req;
      this.os = os;
      this.con_to = con_to;

      if (os == null)
         bos = new ByteArrayOutputStream();

      if (log.isDebugEnabled())
      {
         log.debug("Stream ready for writing");
         if (bos != null)
            log.debug("Buffering all data before sending request");
      }
   }

   /**
    * Setup this stream to dump the data to the great bit-bucket in the sky.
    * This is needed for when a module handles the request directly.
    *
    * @param req the request this stream is to be associated with
    */
   void ignoreData(Request req)
   {
      this.req = req;
      ignore = true;
   }

   /**
    * Return the response we got from sendRequest(). This waits until the
    * request has actually been sent.
    *
    * @return the response returned by sendRequest()
    */
   synchronized Response getResponse()
   {
      while (resp == null)
         try
         {
            wait();
         }
         catch (InterruptedException ie)
         {
         }

      return resp;
   }

   /**
    * Returns the number of bytes this stream is willing to accept, or -1 if it
    * is unbounded.
    *
    * @return the number of bytes
    */
   public int getLength()
   {
      return length;
   }

   /**
    * Gets the trailers which were set with <code>setTrailers()</code>.
    *
    * @return an array of header fields
    * @see #setTrailers(HTTPClient.NVPair[])
    */
   public NVPair[] getTrailers()
   {
      return trailers;
   }

   /**
    * Sets the trailers to be sent if the output is sent with the chunked
    * transfer encoding. These must be set before the output stream is closed
    * for them to be sent.
    * <P>
    * Any trailers set here <strong>should</strong> be mentioned in a
    * <var>Trailer</var> header in the request (see section 14.40 of
    * draft-ietf-http-v11-spec-rev-06.txt).
    * <P>
    * This method (and its related <code>getTrailers()</code>)) are in this
    * class and not in <var>Request</var> because setting trailers is something
    * an application may want to do, not only modules.
    *
    * @param trailers an array of header fields
    */
   public void setTrailers(NVPair[] trailers)
   {
      if (trailers != null)
         this.trailers = trailers;
      else
         this.trailers = empty;
   }

   /**
    * Reset this output stream, so it may be reused in a retried request. This
    * method may only be invoked by modules, and <strong>must</strong> never be
    * invoked by an application.
    */
   public void reset()
   {
      rcvd = 0;
      req = null;
      resp = null;
      os = null;
      bos = null;
      con_to = 0;
      ignore = false;
   }

   /**
    * Writes a single byte on the stream. It is subject to the same rules as
    * <code>write(byte[], int, int)</code>.
    *
    * @param b the byte to write
    * @exception IOException if any exception is thrown by the socket
    * @see #write(byte[], int, int)
    */
   public void write(int b) throws IOException, IllegalAccessError
   {
      byte[] tmp = {(byte)b};
      write(tmp, 0, 1);
   }

   /**
    * Writes an array of bytes on the stream. This method may not be used until
    * this stream has been passed to one of the methods in HTTPConnection (i.e.
    * until it has been associated with a request).
    *
    * @param buf an array containing the data to write
    * @param off the offset of the data whithin the buffer
    * @param len the number bytes (starting at <var>off</var>) to write
    * @exception IOException if any exception is thrown by the socket, or if
    *            writing <var>len</var> bytes would cause more bytes to be
    *            written than this stream is willing to accept.
    * @exception IllegalAccessError if this stream has not been associated with
    *            a request yet
    */
   public synchronized void write(byte[] buf, int off, int len) throws IOException, IllegalAccessError
   {
      if (req == null)
         throw new IllegalAccessError("Stream not associated with a request");

      if (ignore)
         return;

      if (length != -1 && rcvd + len > length)
      {
         IOException ioe = new IOException("Tried to write too many bytes (" + (rcvd + len) + " > " + length + ")");
         req.getConnection().closeDemux(ioe, false);
         req.getConnection().outputFinished();
         throw ioe;
      }

      try
      {
         if (bos != null)
            bos.write(buf, off, len);
         else if (length != -1)
            os.write(buf, off, len);
         else
            os.write(Codecs.chunkedEncode(buf, off, len, null, false));
      }
      catch (IOException ioe)
      {
         req.getConnection().closeDemux(ioe, true);
         req.getConnection().outputFinished();
         throw ioe;
      }

      rcvd += len;
   }

   /**
    * Closes the stream and causes the data to be sent if it has not already
    * been done so. This method <strong>must</strong> be invoked when all data
    * has been written.
    *
    * @exception IOException if any exception is thrown by the underlying
    *            socket, or if too few bytes were written.
    * @exception IllegalAccessError if this stream has not been associated with
    *            a request yet.
    */
   public synchronized void close() throws IOException, IllegalAccessError
   {
      if (req == null)
         throw new IllegalAccessError("Stream not associated with a request");

      if (ignore)
         return;

      if (bos != null)
      {
         req.setData(bos.toByteArray());
         req.setStream(null);

         if (trailers.length > 0)
         {
            NVPair[] hdrs = req.getHeaders();

            // remove any Trailer header field

            int len = hdrs.length;
            for (int idx = 0; idx < len; idx++)
            {
               if (hdrs[idx].getName().equalsIgnoreCase("Trailer"))
               {
                  System.arraycopy(hdrs, idx + 1, hdrs, idx, len - idx - 1);
                  len--;
               }
            }

            // add the trailers to the headers

            hdrs = Util.resizeArray(hdrs, len + trailers.length);
            System.arraycopy(trailers, 0, hdrs, len, trailers.length);

            req.setHeaders(hdrs);
         }

         if (log.isDebugEnabled())
            log.debug("Sending request");

         try
         {
            resp = req.getConnection().sendRequest(req, con_to);
         }
         catch (ModuleException me)
         {
            throw new IOException(me.toString());
         }
         notify();
      }
      else
      {
         if (rcvd < length)
         {
            IOException ioe =
               new IOException("Premature close: only " + rcvd + " bytes written instead of the " + "expected "
                  + length);
            req.getConnection().closeDemux(ioe, false);
            req.getConnection().outputFinished();
            throw ioe;
         }

         try
         {
            if (length == -1)
            {

               if (log.isDebugEnabled() && trailers.length > 0)
               {
                  log.debug("Sending trailers:");
                  for (int idx = 0; idx < trailers.length; idx++)
                     log.debug("\t" + trailers[idx].getName() + ": " + trailers[idx].getValue());
               }

               os.write(Codecs.chunkedEncode(null, 0, 0, trailers, true));
            }

            os.flush();

            if (log.isDebugEnabled())
               log.debug("All data sent");

         }
         catch (IOException ioe)
         {
            req.getConnection().closeDemux(ioe, true);
            throw ioe;
         }
         finally
         {
            req.getConnection().outputFinished();
         }
      }
   }

   /**
    * produces a string describing this stream.
    *
    * @return a string containing the name and the length
    */
   public String toString()
   {
      return getClass().getName() + "[length=" + length + "]";
   }
}
